<?php
/**
 * @see Alinks_Keyphrase
 */
require_once('Alinks/Keyphrase.php');

/**
 * @see Alinks_Ajax
 */
require_once('Alinks/Ajax.php');

/**
 * @see Alinks_Ajax_Callable_Interface
 */
require_once('Alinks/Ajax/Callable/Interface.php');

/**
 * Used for getting and setting keyphrases in the database
 *
 * This class is pretty much the gate keeper for keyphrases. It deals with
 * getting and setting (and deleting) keyphrases from the database. All
 * keyphrase & database operations should be taking place through this class.
 *
 * Copyright (C) 2008  Sean Hickey
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Sean Hickey <sean@headzoo.com>
 * @version $Id: Keeper.php 425 2008-07-09 16:27:54Z sean $
 * @package Alinks_Keyphrase
 */
class Alinks_Keyphrase_Keeper 
	implements SplObserver, Alinks_Ajax_Callable_Interface
{
	/**
	 * The caching key and namespace
	 */
	const CACHE_KEY = 'all';
	const CACHE_NS = 'keyphrases';
	
	/**
	 * Instance of wpdb
	 *
	 * @var wpdb
	 */
	protected $_wpdb;
	
	/**
	 * Instance of Alinks_Cacher_Interface
	 *
	 * @param Alinks_Cacher_Interface
	 */
	protected $_cacher;
	
	/**
	 * Keyphrases that need to be updated
	 *
	 * @var Alinks_Keyphrase[]
	 */
	protected $_to_update = array();
	
	/**
	 * Whether the next method call is coming from an Ajax
	 * request
	 *
	 * @var bool
	 */
	protected $_ajax_calling = false;
	
	/**
	 * Whether the next method call is coming from an XML-RPC
	 * request
	 *
	 * @var bool
	 */
	protected $_xmlrpc_calling = false;
	
	/**
	 * Constructor
	 *
	 * @param wpdb $wpdb Instance of wpdb
	 * @param string $table_prefix The DB table prefix
	 * @param Alinks_Cacher_Interface $cacher Caching object
	 */
	public function __construct(wpdb $wpdb, $table_prefix, Alinks_Cacher_Interface $cacher)
	{
		$wpdb->alinks_keyphrases = $table_prefix . 'alinks_keyphrases';
		$wpdb->alinks_k_options = $table_prefix . 'alinks_k_options';
		$this->_wpdb = $wpdb;
		$this->_cacher = $cacher;
	}
	
	/**
	 * Updates any keyphrases that have changed
	 *
	 * This class usually attaches itself as an observer to the keyphrases
	 * that are created. When a keyphrase's state changes, this class instance
	 * is notified, and makes note of the keyphrase.
	 * This method is called when WP is shutting down, and the method will finally
	 * write any keyphrase changes to the database.
	 */
	public function shutdown()
	{
		foreach($this->_to_update as $subject) {
			$phrase 		= $this->_wpdb->escape($subject->getPhrase());
			$description 	= $this->_wpdb->escape($subject->getDescription());
			$factory 		= $this->_wpdb->escape($subject->getFactory());
			$created 		= date('Y-m-d H:i:s', $subject->getCreated());
			$hash 			= $this->_wpdb->escape($subject->getHash());
			
			$sql = "INSERT INTO {$this->_wpdb->alinks_keyphrases}
			(`phrase`, `description`, `factory`, `created`, `hash`)
			VALUES('$phrase', '$description', '$factory', '$created', '$hash' )
			ON DUPLICATE KEY UPDATE
			`phrase` = '$phrase',
			`description` = '$description',
			`factory` = '$factory',
			`created` = '$created'";
			$results = $this->_query($sql);
			
			foreach($subject->getOptions() as $key => $value) {
				$key = $this->_wpdb->escape($key);
				$value = $this->_wpdb->escape($value);
				$sql = "INSERT INTO {$this->_wpdb->alinks_k_options}
				(`keyphrase_hash`, `key`, `value`)
				VALUES('$hash', '$key', '$value')
				ON DUPLICATE KEY UPDATE `value` = '$value'";
				$this->_query($sql);
			}
		}
		
		if (!empty($this->_to_update)) {
			$this->_cacher->flush(self::CACHE_NS);
			/**
			 * @see Alinks_Post_Parser
			 */
			require_once('Alinks/Post/Parser.php');
			$this->_cacher->flush(Alinks_Post_Parser::CACHE_NS_POSTS);
			$this->_cacher->flush(Alinks_Post_Parser::CACHE_NS_COMMENTS);
		}
	}
	
	/**
	 * Creates a new keyphrase belonging to the specific factory
	 *
	 * Returns a new instance of Alinks_Keyphrase that belongs to the
	 * specified factory.
	 *
	 * @param string $factory The factory safe name
	 * @return Alinks_Keyphrase
	 */
	public function create($factory)
	{
		$keyphrase = new Alinks_Keyphrase($factory);
		$keyphrase->attach($this);
		return $keyphrase;
	}
	
	/**
	 * Returns all the defined keyphrases
	 *
	 * @param bool $long_factory_names Whether to return long factory names
	 * @return Alinks_Keyphrase[]
	 */
	public function fetchAll($long_factory_names = false)
	{
		if ($this->_cacher->exists(self::CACHE_KEY, self::CACHE_NS) && !Alinks_Registry::get('debugging')) {
			$results = $this->_cacher->get(self::CACHE_KEY, self::CACHE_NS);
		} else {
			$sql = "SELECT k.*, o.key, o.value FROM {$this->_wpdb->alinks_keyphrases} AS k
				LEFT JOIN {$this->_wpdb->alinks_k_options} AS o ON o.keyphrase_hash = k.hash
				ORDER BY k.hash ASC";
			$results = $this->_query($sql);
			$this->_cacher->set(self::CACHE_KEY, self::CACHE_NS, $results);
		}

		if ($long_factory_names) {
			$factories = Alinks_Registry::get('factories');
		}
		
		$keyphrases = $this->_resultsToKeyphrases($results);
		
		/**
		 * Check if this method is being called from an Ajax request.
		 */
		if ($this->_ajax_calling) {
			for($i = 0, $t = count($keyphrases); $i < $t; $i++) {
				$keyphrases[$i] = $keyphrases[$i]->toStdClass();
				$keyphrases[$i]->created = date('Y-m-d', $keyphrases[$i]->created);
				if ($long_factory_names) {
					if (!isset($factories[$keyphrases[$i]->factory])) {
						continue;
					}
					$keyphrases[$i]->factory = $factories[$keyphrases[$i]->factory]->getName();
				}
			}
			Alinks_Ajax::sendResponse(true, null, array('identifier' => 'hash', 'items' => $keyphrases));
		}
		
		return $keyphrases;
	}
	
	/**
	 * Returns the keyphrases with the given hashes
	 *
	 * Given an array of keyphrase hashes, this method returns the
	 * those keyphrases.
	 *
	 * @param array $hashes The hashes
	 * @return Alinks_Keyphrase[]
	 */
	public function fetchByHashes(array $hashes)
	{
		$keyphrases = $this->fetchAll();
		$matching_keyphrases = array();
		foreach($keyphrases as $keyphrase) {
			if (in_array($keyphrase->getHash(), $hashes)) {
				$matching_keyphrases[] = $keyphrase;
			}
		}
		
		return $matching_keyphrases;
	}
	
	/**
	 * Returns a single keyphrase with the given hash
	 *
	 * @param string $hash The hash
	 * @return Alinks_Keyphrase
	 */
	public function fetchByHash($hash)
	{
		$hash = $this->_wpdb->escape($hash);
		$sql = "SELECT k.*, o.key, o.value FROM {$this->_wpdb->alinks_keyphrases} AS k
			LEFT JOIN {$this->_wpdb->alinks_k_options} AS o ON o.keyphrase_hash = k.hash
			WHERE `hash` = '$hash'
			ORDER BY k.hash ASC";
		$results = $this->_query($sql);
		$keyphrases = $this->_resultsToKeyphrases($results);
		if (count($keyphrases) == 0) {
			/**
			 * @see Alinks_Exception
			 */
			require_once('Alinks/Exception.php');
			throw new Alinks_Exception("There is no keyphrase for the hash $hash");
		}
		
		return $keyphrases[0];
	}
	
	/**
	 * Returns all the keyphrases defined by the given factory
	 *
	 * @param string $factory The factory safe name
	 * @return Alinks_Keyphrase[]
	 */
	public function fetchByFactory($factory)
	{
		$factory = $this->_wpdb->escape($factory);
		$sql = "SELECT k.*, o.key, o.value FROM {$this->_wpdb->alinks_keyphrases} AS k
			LEFT JOIN {$this->_wpdb->alinks_k_options} AS o ON o.keyphrase_hash = k.hash
			WHERE `factory` = '$factory'
			ORDER BY k.hash ASC";
		$results = $this->_query($sql);
		$keyphrases = $this->_resultsToKeyphrases($results);

		return $keyphrases;
	}
	
	/**
	 * Deletes keyphrases by hashes
	 *
	 * Given an array of hashes, this method delets all the specified
	 * keyphrases.
	 *
	 * @param array $hashes The keyphrase hashes
	 * @return bool
	 */
	public function deleteByHashes($hashes)
	{
		foreach($hashes as $hash) {
			$hash = $this->_wpdb->escape($hash);
			$this->_query("DELETE FROM {$this->_wpdb->alinks_keyphrases} WHERE `hash` = '$hash' LIMIT 1");
			$this->_query("DELETE FROM {$this->_wpdb->alinks_k_options} WHERE `keyphrase_hash` = '$hash' LIMIT 1");
		}
		$this->_cacher->flush(self::CACHE_NS);
		/**
		 * @see Alinks_Post_Parser
		 */
		require_once('Alinks/Post/Parser.php');
		$this->_cacher->flush(Alinks_Post_Parser::CACHE_NS_POSTS);
		$this->_cacher->flush(Alinks_Post_Parser::CACHE_NS_COMMENTS);
		
		if ($this->_ajax_calling) {
			Alinks_Ajax::sendResponse(true, 'Keyphrase(s) deleted!');
		}
		
		return true;
	}
	
	/**
	 * Implements SplObserver::update()
	 */
	public function update(SplSubject $subject)
	{
		if (!isset($this->_to_update[$subject->getHash()])) {
			$this->_to_update[$subject->getHash()] = $subject;
		}
	}
	
	/**
	 * Returns the keyphrase form filled in with the given keyphrase
	 *
	 * @param string $hash The hash for the keyphrase to edit
	 * @return Alinks_Form
	 */
	public function editKeyphrasesForm($hash)
	{
		$keyphrase = $this->fetchByHash($hash);
		$factory = Alinks_Registry::get('Alinks_Factory_Keeper')->fetchAll($keyphrase->getFactory());
		$form = $factory->getKeyphrasesForm();
		$hash_element = new Alinks_Form_Element_Hidden('hash');
		$form->addElement($hash_element);
		$factory_element = new Alinks_Form_Element_Hidden('factory');
		$form->addElement($factory_element);
		
		foreach($form->getElements() as $element) {
			if ($element->getName() == 'phrase') {
				$element->setValue($keyphrase->getPhrase());
			} else if ($element->getName() == 'description') {
				$element->setValue($keyphrase->getDescription());
			} else if ($element->getName() == 'hash') {
				$element->setValue($keyphrase->getHash());
			} else if ($element->getName() == 'factory') {
				$element->setValue($keyphrase->getFactory());
			} else {
				$value = $keyphrase->getOptions($element->getName());
				$element->setValue($value);
			}
		}
		
		if ($this->_ajax_calling) {
			/**
			 * @see Alinks_Form_Json
			 */
			require_once('Alinks/Form/Json.php');
			$form = new Alinks_Form_Json($form);
		
			Alinks_Ajax::sendResponse(true, null, array(
				'form' => $form->getJson()
			));
		}
		
		return $form;
	}
	
	/**
	 * Updates a keyphrase submitted from the keyphrases form
	 *
	 * @return bool
	 */
	public function updateFromKeyphrasesForm()
	{
		if (empty($_POST['hash']) && empty($_POST['factory'])) {
			return;
		}
		if (empty($_POST['hash'])) {
			$keyphrase = $this->create($_POST['factory']);
		} else {
			$keyphrase = $this->fetchByHash($_POST['hash']);
		}
		
		$error_msg = null;
		try {
			$keyphrase->setPhrase($_POST['phrase']);
			$keyphrase->setDescription($_POST['description']);
			unset($_POST['phrase']);
			unset($_POST['description']);
			
			$form = Alinks_Registry::get('Alinks_Factory_Keeper')->fetchAll($_POST['factory'])->getKeyphrasesForm();
			foreach($form->getElements() as $key => $element) {
				if (isset($_POST[$key])) {
					$keyphrase->addOption($key, $_POST[$key]);
				}
			}
		} catch (Alinks_Exception $e) {
			if (!$this->_ajax_calling) {
				throw $e;
			}
			$error_msg = $e->getMessage();
		}
		
		if ($this->_ajax_calling) {
			if (null !== $error_msg) {
				Alinks_Ajax::sendResponse('false', $error_msg);
			} else {
				$this->shutdown(); // <- Apparently not called at the end of Ajax requests
				Alinks_Ajax::sendResponse(true, 'Keyphrase updated!');
			}
		}
		
		return true;
	}
	
	/**
	 * Implements Alinks_Ajax_Callable_Interface::setAjaxCalling
	 */
	public function setAjaxCalling()
	{
		$this->_ajax_calling = true;
	}
	
	/**
	 * Queries the database and returns the results
	 *
	 * A wrapper for wpdb::get_results(). This method traps any errors, and
	 * throws an Alinks_Exception.
	 *
	 * @param string $sql The SQL
	 * @return array
	 */
	protected function _query($sql)
	{
		$show_errors = $this->_wpdb->show_errors;
		$this->_wpdb->show_errors = false;
		$results = @$this->_wpdb->get_results($sql, ARRAY_A);
		$this->_wpdb->show_errors = $show_errors;
		
		if (null === $results && mysql_errno()) {
			/**
			 * @see Alinks_Db_Exception
			 */
			require_once('Alinks/Db/Exception.php');
			throw new Alinks_Db_Exception(mysql_error(), mysql_errno());
		}
		
		return $results;
	}
	
	/**
	 * Converts database results into an array of Alinks_Keyphrase instances
	 *
	 * Returns an empty array if there are no keyphrases in the results.
	 *
	 * @param array $results The database results
	 * @return Alinks_Keyphrase[]
	 */
	protected function _resultsToKeyphrases(array $results)
	{
		if (!$results) {
			return array();
		}
		
		/**
		 * A typical result set looks like this:
		 *
		 * Phrase		Description		Factory		Created		Hash		Key		Value
		 * ------		-----------		-------		-------		----		---		-----
		 * aLinks		A plugin		default		1234455		132a-da		url		http://alinks.headzoo.com
		 * aLinks		A plugin		default		1234455		132a-da		target	_blank
		 *
		 * There are multiple rows for each keyphrase, because each keyphrase has
		 * multiple options.
		 */
		
		$keyphrase = null;
		$keyphrases = array();
		$result = current($results);
		$current_hash = null;
		
		do {
			if ($result['hash'] == $current_hash) {
				$keyphrase->addOption($result['key'], $result['value']);
			} else {
				$keyphrase = $this->create($result['factory']);
				$keyphrase->setPhrase($result['phrase']);
				$keyphrase->setDescription($result['description']);
				$keyphrase->setCreated($result['created']);
				$keyphrase->setHash($result['hash']);
				$keyphrase->addOption($result['key'], $result['value']);
				$keyphrases[] = $keyphrase;
				$current_hash = $result['hash'];
			}
			
		} while(($result = next($results)));
		
		return $keyphrases;
	}
}